function Class(o, F) {
	if (!F) return o;
	F.prototype = o;
	F.prototype.constructor = F;
	F.prototype.superClass = o;
	return F;
}


//globals
var $lastPinned = null;
var maxZIndex = 1000;


var D3MIGrid = Class(function( domsel, data, columns, options) {
	////////////////////////////////////////////////////////////////////////////
	// constructor arguments
	////////////////////////////////////////////////////////////////////////////
	this.domsel = domsel;
	this.data = data;
	this.options = {
		//defaults
		editable: false,
		enableAddRow: false,
		enableCellNavigation: true,
		enableColumnReorder: false,
		forceFitColumns: true
	};
	if (options)
		for (k in options) this.options[k] = options[k];

	
	////////////////////////////////////////////////////////////////////////////
	// public interface
	////////////////////////////////////////////////////////////////////////////
	this.doSearch = function() {
		window.clearTimeout(h_runsearchfilters);
		h_runsearchfilters = window.setTimeout(filterAndUpdate, 10);
	}
	

	////////////////////////////////////////////////////////////////////////////
	// create grid
	////////////////////////////////////////////////////////////////////////////
	this.dataView = new Slick.Data.DataView({ inlineFilters: true });
	this.grid = new Slick.Grid(this.domsel+' .grid-container', this.dataView, columns, this.options);
	
	
	////////////////////////////////////////////////////////////////////////////
	// PRIVATE
	////////////////////////////////////////////////////////////////////////////

	//closure scope
	var that = this;	
	
	var h_runsearchfilters = null;

	//wire up model events to drive the grid (slickgrid magic)
	this.dataView.onRowCountChanged.subscribe(function (e, args) {
		that.grid.updateRowCount();
		that.grid.render();
	});	
	this.dataView.onRowsChanged.subscribe(function (e, args) {
		that.grid.invalidateRows(args.rows);
		that.grid.render();
	});
	
	
	////////////////////////////////////////////////////////////////////////////
	// overlays
	////////////////////////////////////////////////////////////////////////////
	
	var $canvas = $(that.grid.getCanvasNode());
	var $highlightedRow = $(undefined);
	var detailsShowing = null;
	
	
	var c_handle = null;
	//mouseover event on grid changes static overlay
	this.grid.onMouseEnter.subscribe( function (e) {
		window.clearTimeout(h_runsearchfilters);
		var rc = that.grid.getCellFromEvent(e);

		window.clearTimeout(c_handle);
		c_handle = window.setTimeout(function(){staticOverlayDetails(rc.row);}, 30);	
	});
				
	this.grid.onActiveCellChanged.subscribe( function (e) {
		var activeCell = that.grid.getActiveCell();
		staticOverlayDetails(activeCell.row);
	});
	
	function staticOverlayDetails(rown) {
		var o = that.grid.getData().getItem(rown);
		if (!o) return;

		//highlight grid row
		$highlightedRow.removeClass("active");
		$highlightedRow = $canvas.find(".slick-row[row="+rown+"]").addClass("active");
		//this line is REAAALLLLY SLOOOOOW on IE8 ^
		
		if (detailsShowing != o)
			$(that.domsel+' .fixed-overlay').html(that.renderOverlay(o));			
		detailsShowing = o;
	}

	
	//click event on grid opens new overlay
	this.grid.onClick.subscribe( function (e) { 
		var rc = that.grid.getCellFromEvent(e);
		popupOverlayDetails(rc.row);
	});
	
	function popupOverlayDetails(rown) {
		var o = that.grid.getData().getItem(rown);
		if (!o) return;
		
		//render element
		var $p = $('<div class="overlay">').html(that.renderOverlay(o))
		
		//focus
		.css('zIndex', ++maxZIndex)
		
		//focus on click/startdrag
		.mousedown(function(e){
			$( this ).css('zIndex', ++maxZIndex);
		})
		//on drag
		.drag(function( ev, dd ){
		      $( this ).css({
				      top: dd.offsetY,
				      left: dd.offsetX
		      });
		      if ($(this).is($lastPinned))
			      $lastPinned = null;
		});
		
		//concertina if previous overlay has not been moved
		if ($lastPinned) {
			$p.css({
			  top: String(parseInt($lastPinned.css('top').replace('px','')) + 20 )+'px',
			  right: String(parseInt($lastPinned.css('right').replace('px','')) + 20 )+'px'
			});
		}
		$lastPinned = $p;
		
		//append to body
		$('body').append($p);
		
		//button to remove it (must come after its appended to body)
		$p.find('.overlay-pin').show().click(function(e) {
			$p.remove();		
		});
	}
	

	////////////////////////////////////////////////////////////////////////////
	// grid sort
	////////////////////////////////////////////////////////////////////////////
	
	var currentSortCmp = null;	
	this.grid.onSort.subscribe(function (e, args) {
		if (that.preSort)
			that.preSort(e, args);
		
		// declarations for closure
		var field = args.sortCol.field;
		var sign = args.sortAsc ? 1 : -1;
		var prevSortCmp = currentSortCmp;
		
		// store closure in global
		currentSortCmp = function (dataRow1, dataRow2) {
			
			var value1 = dataRow1[field], value2 = dataRow2[field];
			
			//if equal then sort in previous scope (recurring)
			if (value1 == value2 && prevSortCmp)
				return prevSortCmp(dataRow1, dataRow2);
				
			return (value1 == value2 ? 0 : (value1 > value2 ? 1 : -1)) * sign;
		};
		that.dataView.sort(currentSortCmp);
		
		that.grid.invalidate();
		that.grid.render();		
	});

	
	////////////////////////////////////////////////////////////////////////////
	// grid filters
	////////////////////////////////////////////////////////////////////////////
	function filterAndUpdate() {
		var args = that.getSearchArgs(that.domsel);		
		
		var renderedRange = that.grid.getRenderedRange();		
		that.dataView.setFilterArgs(args);
		that.dataView.setRefreshHints({
			ignoreDiffsBefore: renderedRange.top,
			ignoreDiffsAfter: renderedRange.bottom + 1
		});
		
		//do it
		that.dataView.refresh();
		
		//display first result
		var result0 = that.grid.getData().getItem(0);
		if (result0)
			staticOverlayDetails(0);
	}

		
	$(window).resize(function() {
		that.grid.resizeCanvas();
	});

	////////////////////////////////////////////////////////////////////////////
	// final init
	////////////////////////////////////////////////////////////////////////////
	//required in subclass:
	//  this.getSearchArgs = settings.getSearchArgs;
	//  this.searchFilter = settings.searchFilter;
	//  this.renderOverlay = settings.renderOverlay;
	
	//optional in subclass:
	//  this.preSort = settings.preSort;
	
	this.init = function() {
		
		//populate grid
		this.dataView.beginUpdate();
		this.dataView.setItems(this.data);
		this.dataView.setFilter(this.searchFilter);
		this.dataView.setFilterArgs(this.getSearchArgs(this.domsel));
		this.dataView.endUpdate();
		
		//simulate click to order by first column
		$(this.domsel+" .slick-header-column:first").trigger('click');
		
		//display first item
		staticOverlayDetails(0);
	}
});





	

	
